chore: promote dev to main for 0.19.0 - #995
Merged
Merged
Conversation
…store Ref #955 -- closes at promotion. Channel tests that exercise the post path without isolating resolution fall through to tier-2 resolution and write fixture messages into the real home-level channel store. The override to prevent that already existed; what was missing is that nothing FAILED when a test reached the real store, so the contamination was silent and cumulative. Root cause found while building this: four test files "isolate" themselves by setting SYNAPT_PROJECT_DIR or SYNAPT_DATA_DIR. Neither name is read anywhere in the package. Setting them produces no error and no warning, so the tests have looked isolated for their whole lifetime while resolving to the live store. An isolation step that cannot fail is not an isolation step. Two layers: Layer 1 covers the import window only. pytest_configure runs before any test module is imported, so module-level and collection-time code needs somewhere safe to resolve; an autouse fixture then hands ordinary resolution back per test. That narrowness was learned, not designed -- leaving a session-wide SYNAPT_SHARED_CHANNELS_DIR in place broke thirty tests, because the override is tier 1 and outranks the tier-3 local resolution some tests deliberately exercise, and one shared directory turns independent tests into shared-state ones. An environment default is a semantic change to path resolution, not a neutral safety net. Layer 2 refuses without redirecting, so it does not change where anything legitimately resolves. Production gains a no-op policy seam consulted before any store directory is created or file opened; with no policy installed behaviour is unchanged. The seam sits at resolution, not only at the write calls: _channels_dir mkdirs the global directory as part of resolving it, so a guard at the append seam would report a write rather than prevent one. Five write surfaces are covered, including one that composes the store path itself and so is reachable by neither the resolver guard nor the explicit-argument guard. Also isolates the fourteen tests the guard caught resolving to the live store, switching them to the override the resolver actually consults. Measured on one runner, whole suite, BOTH SIDES AT BASE 1245f62: base 1245f62, no guard 23 failed / 2768 passed this commit, guard armed 23 failed / 2792 passed Net new failures zero. The +24 passed is exactly the 24 witnesses this commit adds. The 23 failures are pre-existing at this base and untouched here. An earlier draft of this message quoted 2774 passed and claimed one incidental fix. Both came from runs taken at the previous base and did not survive re-measurement here: the failure that appeared "fixed" was already fixed upstream. Numbers from two different bases cannot be one delta, and a pair that does not reconcile is a pair to re-measure rather than to explain. The data-root half ships behind --strict-recall-data-root, off by default. Arming it fails 30 further tests across 10 files, and those are true positives: recall tests deliberately strip the root override to measure path inference, and that inference currently lands in a real checkout. Isolating those sites is separate reviewable work; the mechanism ships built and witnessed rather than absent, with the count reported rather than quietly carried. Premium boundary: recall is OSS -- this is test infrastructure plus a neutral, no-op policy seam on local channel and data path resolution. No identity, no org semantics, no entitlement behaviour.
Review response on the store-isolation guard. Ref #955 -- closes at promotion. r2 found the seam covered the JSONL surface and not the SQLite one, proven with a recording policy: channel_post consulted it, the SQLite paths did not. The finding was right; the two SQLite paths turn out to need opposite treatment, so each gets the one the evidence assigns it. _open_state_db IS guarded. It takes its path from the caller, exactly the shape already recognised for an explicit channels_dir, so resolution never sees it -- and unlike the per-gripspace database it can land inside the global store. A probe put a caller-supplied path there with the policy consulted zero times before the file was created. Now refused, with a witness that asserts the file does not exist afterwards. _db_path is deliberately NOT guarded by the channel policy, and the docstring now says so. It is composed from project_data_dir, so it cannot resolve inside the global store: probed directly, and probed again with the data root forced at the parent of the protected root, and it stayed outside both times. A channel-store check there could not refuse anything. Adding one would read as tightening coverage while installing a check that cannot fail, which is the defect this seam exists to prevent. Its real coverage is the data-root policy, which project_data_dir consults -- verified by probe, not assumed. A witness pins both halves so the asymmetry is not "fixed" later by someone reading the seam list and seeing an oversight. Separately, r2 showed importlib.reload silently clears both policies: armed before, disarmed after, no error. That cannot be fixed where it happens, because a reload replaces the module wholesale. Patching the two known mechanisms is the weaker move -- the next one arrives unannounced with the same signature, a green suite and absent protection. So the policy is re-armed before every test instead, which closes the class rather than the instances. Cost is one identity check against None. Whole suite at base 1245f62: 23 failed / 2795 passed, against a 23 / 2768 baseline. Net new failures zero; the +27 passed is exactly the 27 witnesses. Guarding the state-db surface surfaced no further true positives. Premium boundary: recall is OSS -- test infrastructure plus the same neutral, no-op policy seam. No identity, no org semantics, no entitlement behaviour.
…on-guard test(recall): fail loudly when a test resolves into the live channel store
…e armed Ref #967 -- closes at promotion. Phase 1 of the burn-down. The data-root half of the store-isolation guard shipped behind --strict-recall-data-root, off by default, because arming it failed 30 tests across 10 files. Those were true positives: tests/recall deliberately strips the root override so tests measure path INFERENCE, and that inference resolved into a real checkout. Every one of the 30 read or wrote the operator's live .synapt/recall. The count was re-derived at this base before any edit rather than carried forward from the issue. It came back 30 with an identical per-file distribution, but the check was not ceremonial: the original figure was taken before the guard merged, so it was measured under a configuration that no longer existed. Isolation preserves the fixture's intent instead of overriding it. The autouse fixture strips AMBIENT overrides so these tests measure inference, and that is correct; its own docstring already carved out the other half -- "unless a test sets the override itself." So two mechanisms, chosen per site by what the test is actually asserting: A test that NEEDS a store gets an owned root, via a new owned_recall_root fixture for pytest-style tests and an owned_store helper for the unittest.TestCase classes that cannot take fixtures. One helper rather than a hand-rolled save/restore per class, because five copies of an environment restore is five chances to restore the wrong thing. A test that is genuinely measuring inference keeps measuring it, and moves its CWD to a directory it owns instead. recall_save hardcodes project = Path.cwd(), so it has no project_dir to pass and the environment override does not apply to an explicit path; redirecting the root there would have looked like isolation while changing nothing. One site was a real bug rather than missing isolation: two lines called the live _reminders_path() and mkdir'd its parent inside the operator's store, then discarded the value, because the patch immediately below supplies the path actually used. Dead code that wrote to a real location while reading as setup for a temporary one. Whole suite on one runner at this base: before, default 23 failed / 2795 passed before, strict 53 failed / 2765 passed after, default 23 failed / 2795 passed after, strict 23 failed / 2795 passed The burn-down set -- strict minus default -- goes from 30 to 0, and the two modes now produce identical results, which is the property the flip depends on. Zero new failures against the default baseline. The 23 are pre-existing at this base and untouched here. Flipping the default is deliberately NOT in this change. The flag stays off so this can be reviewed as isolation work on its own evidence; the flip is a separate one-line change that now has nothing left to hide behind it. Premium boundary: recall is OSS -- test isolation only. No production code is touched by this commit. No identity, no org semantics, no entitlement behaviour.
…ndown test(recall): isolate the 30 data-root sites so the strict flag can be armed
Ref #967 -- closes at promotion. The 30 sites that resolved into a real checkout are isolated, so the guard no longer has anything to break. --strict-recall-data-root is now the default and --no-strict-recall-data-root is the debugging escape. THE EVIDENCE FOR THIS FLIP IS NOT THAT BOTH MODES AGREE. Before the burn-down, running with the flag differed from running without it, and that difference was the proof the flag was wired. Now they agree -- which is the success criterion and, identically, the signature of a guard that has stopped working. Modal agreement can no longer tell those apart, so citing it would be vacuous by construction. The evidence is two direct witnesses instead. One asserts the policy is installed with no flag passed and that an unowned path is still refused. The other asserts the escape hatch reaches the install decision, in two separately checkable links -- the option resolving to the destination the installer reads, and the installer declining to arm when told not to -- because either link alone is satisfiable without the other. The escape hatch also has to leave a GREEN suite or nobody will use it, so the armed-by-default witness skips with a stated reason under --no-strict-recall-data-root rather than failing. A debugging flag that reddens the suite is a flag people route around. Neither witness is a nested pytest run. pytester executes in-process and shares module globals, which is how this guard silently disarmed itself once already; a nested run to test disarming would aim that same hazard at the thing it broke before. Review also surfaced a third link nobody had pinned: when BOTH flags are passed, the last one wins. That was already the behaviour, but by argparse accident rather than by decision -- undocumented, untested, and free to change under a dependency bump with nothing going red. A CI base config carrying one flag and a job appending the other would have had its behaviour settled by argument order nobody wrote down. Last-wins is the conventional answer and a good one; a witness and one line of option help make it a decision instead of an accident. Two further links came from review, both about what the escape hatch is allowed to mean. The skip condition discriminates on the ARGV signal rather than the resolved option. Those agree today and come apart in exactly the case that matters: if the default is ever silently reverted, the resolved option reads off with no flag passed, and skipping there would announce "deliberately disarmed" about a disarm nobody requested. A false reason is worse than no reason, because it explains away the thing it should surface. It now fails and names the reverted default. And the hatch is SCOPED: disarming the data root must leave the channel guard armed. Nothing witnessed that, so the escape was free to widen into a full disarm -- taking a guard that was never opt-in -- while every other witness here still passed, because they arm their own policy or assert against a decoy. An escape hatch nobody has bounded is a hatch that grows. Both were mutation-checked rather than assumed: reverting the default makes the first fail, widening the hatch makes the second fail, and both restore byte-identical. The precedence witness runs in a SUBPROCESS rather than through pytester. The hazard that rules out nested in-process runs here is shared module globals -- a nested run can disarm the outer guard, and a green result would then be indistinguishable from the hazard firing. A subprocess shares no globals, so it is the one shape that can answer an argument-parsing question without risking the thing being asked about. Also consolidates test_codex onto the shared owned_store helper. It carried a hand-rolled two-variable save/restore -- the duplication that helper exists to remove -- because it was written before the helper existed. Whole suite: 23 failed / 2798 passed, against a 23 / 2795 baseline. The +3 are the witnesses this commit adds; the two review links are assertions inside existing witnesses rather than new ones. Zero new failures. Non-emptiness checked: 2797 is the full suite, not a vacuous green over an empty collection. Premium boundary: recall is OSS -- test configuration only. No production code is touched. No identity, no org semantics, no entitlement behaviour.
test(recall): arm the data-root guard by default
Store resolution consulted the gr2 workspace marker before the gr1 membership marker, so a directory carrying both resolved to itself and its declared membership was never read. Its recall data landed where no other surface in that workspace looks, and nothing anywhere reported a problem. The two markers answer different questions. ".grip" asks "am I a workspace containing units?"; ".gitgrip/griptree.json" asks "whose larger whole am I a part of?". Store resolution needs the second, because a member's data belongs to the workspace it is a member of. Membership is now consulted first, and where it is asserted but cannot be verified, resolution falls through to the locality marker rather than reporting nothing. Reordering alone would have made that population WORSE: such a directory used to short-circuit on ".grip" and resolve to itself, and would instead have resolved to nothing -- fragmenting a store that previously cohered, which is the same failure this change exists to prevent. Where membership cannot be verified and there is no locality evidence either, the result is None exactly as before, and the walk deliberately does not continue upward. Continuing would find a better answer for that case, and that is a separate change with its own evidence. The property this one claims is that every directory either improves or is unchanged and nothing else moves -- which stops being checkable the moment it also improves an unrelated case. Five tests, none of which the suite previously had. Every existing case builds one marker or the other, so all 38 passed identically with and without the reordering: the suite could not distinguish fixed from unfixed. The new cases fail with AssertionError against the behaviour they replace, naming the wrong root. Two of the five are controls rather than claims -- one pins that locality is narrowed and not removed, the other pins the no-locality case as unchanged and deliberately NOT improved.
fix(recall): a member's store belongs to the workspace it is a member of
A second consecutive `build` over an unchanged store must do ~zero work, run
fast, and say that it skipped. It does not today. Measured on a frozen
4-transcript corpus (39MB, 1,709 chunks) with mtimes pinned so "unchanged" was
a controlled fact rather than an assumption, a run that parsed ZERO files still
took 40.9s. The build has exactly one change-detector, it works, and it guards
the cheapest stage; every expensive stage downstream is unconditional.
Stage attribution on that do-nothing run: parse 1.2s, full FTS5 re-save 1.2s,
full re-cluster 0.6s, re-tag every cluster, timeline rebuild, and then 37.7s
(92%) in the LLM cluster-summary backlog grinder, which loads a model and makes
network round-trips on every build regardless of whether anything changed.
Specifies, as failing tests:
- parse-skip keys scoped by source dir rather than basename alone. The
manifest is one flat list spanning every archive dir while the skip dict is
keyed on basename, so same-named files shadow each other and the loser can
never match.
- archive freshness on size OR mtime. Freshness is size-only today, so a
same-size content edit is never re-archived and therefore never re-indexed.
- an input signature spanning transcripts, channels and journals, with the
no-op decision derived from it and surviving a manifest round trip.
- the summary grinder off the build path and onto an explicit, bounded
`maintain` command that reports its remaining backlog rather than draining
it silently.
- a no-op run that says it is a no-op.
- incremental as the default with `--full` as the explicit opt-in, and the
CLI and MCP defaults agreeing (they currently disagree).
Every skip assertion is paired with a negative control that changes exactly one
input and requires the build to notice, so the suite cannot be satisfied by an
implementation that simply skips everything. The controls cover a changed
transcript, a new channel message, a new journal entry, a newly arrived file,
and the skip message itself never appearing on a run that had work to do.
20 failing, 6 passing; the 6 are regression guards and controls that already
hold on current behavior. Failing tests are expected on a feature branch.
Unrelated and pre-existing, recorded so the next reader does not attribute it
here: four tests in test_attribution.py fail on clean dev as well, because
TranscriptChunk reads SYNAPT_AGENT_ID from the environment and the test does
not isolate it. Verified by running them against origin/dev with this file
absent. Not fixed here; out of scope.
Premium boundary: OSS. Index build and retrieval only, no identity or org
semantics.
Declares every fixture in the directory synthetic and names the rules for adding more, so file names that describe test roles are never again read as captured data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test: TDD spec for build idempotence
…ance docs: provenance README for tests/fixtures/identify
…pinned locale r2's three non-blocking notes from the live-history merge, each with a born-red witness (the four new tests all red against the pre-hardening _run_git before this change): - subprocess.run now carries a 30s ceiling. A live-per-call primitive whose git blocks on an index.lock otherwise hangs the caller with no way out. Timeout surfaces as the same ValueError contract. - the repository path stays out of error messages. An error that can reach a user-facing surface must not disclose local layout. - git runs under LC_ALL=C. Parsers and error assertions match git's English text, and an unpinned locale is green on this machine and red on a non-English one. tests/recall/test_code_git.py: 18 passed (14 existing untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(recall): code_git hardening — timeout ceiling, path-free errors, pinned locale
fix(journal): retain current next steps after session resume
`incremental` gates exactly one call site today, so a build with nothing to
do still walks channels and journals and still pays for the walk. A real
no-op needs a signal covering every input the build reads.
Adds `build_delta`: a signature over archived transcripts, channel logs and
journals, plus `is_noop` and manifest round-trip helpers.
The dangerous direction here is "up to date", not "slow". A signal watching
only transcripts would let a new channel message or a fresh journal entry go
unindexed while the build reports success -- worse than a slow build, because
the operator sees "nothing to do" and the thing they just wrote is missing
from search with no error anywhere. Every input class is therefore in the
signature and each has its own negative-control witness.
Three properties the witnesses pin, each mutation-verified separately:
- directories are globbed rather than read from a remembered list, so a
newly arrived file changes the signature;
- absence is never a no-op: no previous signature, a malformed one, or a
version mismatch all resolve to doing the work, because the cost of a
wrong None is one extra build and the cost of a wrong match is a build
that never happens;
- entries are sorted before hashing, so the digest depends on the input set
and not on filesystem iteration order.
Stated limit rather than a discovered one: content is not hashed, so an edit
preserving both size and mtime is invisible to this signal. Hashing every
byte on every build would cost more than the build this exists to skip. The
archive layer refreshes on a newer mtime at equal size for the same reason.
Wiring this into the build path lands in the next change of the series.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…roximation
signature_from_manifest promised in its own docstring to return None for
malformed payloads and did not. It accepted six:
files=True, files=False bool subclasses int, so isinstance admits both and
the dataclass field declared int held True
files=-1 nothing bounded the sign
digest=<any non-empty> "x" qualified as a sha256 digest
version=True, version=1.0 found by closing the class rather than the three
reported instances: the guard is a bare != and
True == 1 and 1.0 == 1
Closes the class: version and files must be exactly int (type(...) is int, never
isinstance, with the reason commented at the helper), files must be >= 0, and
digest must match a fully anchored 64-lowercase-hex pattern. Any violation
returns None, which is_noop turns into work.
Scope stated honestly rather than dramatically: no WRONG NO-OP was reachable
through these fields. is_noop compares digests, and a malformed digest cannot
equal a real one, so a stale or garbage payload already resolved to work. What
was reachable is a validator certifying "well-formed" over garbage, and a
declared int holding a bool. A module whose entire argument is that
untrustworthy state must resolve to work does not get to carry a gate that
certifies what it did not check.
Fourteen witnesses. Thirteen malformed payloads, each asserting BOTH halves --
the parse refuses AND is_noop turns the refusal into False -- because a guard
returning None while a caller read None as "unchanged" would satisfy the first
assertion and lose the property it exists to protect. The fourteenth is the
acceptance control: thirteen tests asserting None would all pass against a
validator that rejects everything, which fails in the direction that looks safe.
Mutation-verified per guard, not once for the set, because stacked asserts
short-circuit and one mutation would only prove the first. Reverting the files
guard reddens exactly True/False/-1; reverting the digest guard reddens exactly
the six digest shapes; dropping the version guard reddens exactly the two
version cases. The acceptance control stays green under all three.
feat(build): one signal for whether a build has anything to do
`_archive_and_build` called `upgrade_large_cluster_summaries` on every build. That is unbounded work of a different KIND from everything else in the build path: the rest is local and scales with what changed, while this makes LLM calls and ran regardless. It also sat inside a bare `except: pass`, so a build reported success whether the grinder worked, did nothing, or failed -- the state that makes a cost invisible. Its explicit home is the `maintain` subcommand, which lands later in this series. WHAT IS AND IS NOT LOST MEANWHILE. Summaries come in TWO TIERS, and every loose sentence about this change has been wrong by collapsing them. CONCAT is the baseline and is unaffected: the clustering step earlier in the same function pre-generates a concat summary for EVERY cluster on every build, skipping only those that already hold an LLM one. No cluster is left without a summary by this change. LLM is the upgrade and has two triggers. `process_build_promotions` upgrades by ACCESS TIER and still runs. The removed pass upgraded by SIZE, independent of access -- which is why its query looked for clusters where `method = 'llm'` was absent. So the residual is exactly this: a cluster that is large but rarely searched keeps its concat summary and waits for `maintain` to be UPGRADED to LLM quality. It does not go unsummarized. Two earlier drafts said "summaries stop being generated" and then "no longer gets a summary". Same error twice -- writing "summary" unqualified in a system that has two tiers of them -- and the second was caught in review rather than by me. Satisfies the merged spec `test_build_never_calls_the_summary_grinder`. Mutation-verified: restoring the call turns that witness red. Full suite 11 failed / 2860 passed; every failure is a later-group TDD spec in the same file (maintain, incremental-default, archive-refresh), down from 12. Nothing outside the series changed, and nothing asserted on the removed print.
refactor(build): stop running the summary grinder from a build
Two independent staleness defects, both of which made an incremental build skip a file that had genuinely changed, silently and permanently. Parse-skip keying. `source_files` is one flat list spanning every build source, while the reader keyed `already_indexed` on the BASENAME alone. Two worktrees archiving the same session name produce two entries; under a basename key the second overwrites the first, so the loser's stamp can never match its own file and it re-parses on every incremental build, forever. The key is now (source dir, name), written on both sides -- a reader-only change would have passed its test and stayed inert in production, because manifests never carried the dir. Manifests written before the field keep the old flat key so an upgrade does not force a full rebuild. Archive freshness. `archive_transcripts` skipped whenever the source and the archived copy had equal size, treating equal length as equal content. An edit that preserves byte count is invisible to that check, so it was never re-archived and therefore never re-indexed -- and the docstring asserted the behaviour as intended. mtime is now a second, independent signal, compared in ns so float rounding cannot make an unchanged file look newer than itself. Both negative controls hold: unchanged files stay a no-op, and a shrunken source still does not overwrite a longer archive. Also pins three properties that were correct but unwitnessed, so a later change cannot weaken them silently: - a digest with a trailing newline is rejected, which discriminates the `\Z` anchor from `$` -- `$` matches before a trailing newline, and the suite could not previously tell the two apart - a zero-file signature is accepted, bounding the guard from the side the rejection cases do not reach - is_noop compares the digest and deliberately not the file count, which is derived from the same input rather than independent evidence about it Each new witness is killed by its own mutation and by no other.
fix(build): scope the parse-skip key by source dir, and refresh on mtime
The archive verbs computed Path.cwd() and passed it as the workspace root. project_data_dir consults SYNAPT_RECALL_ROOT only when no root is passed, so forwarding cwd did not merely skip the override, it suppressed it -- the same env var honored by stats and journal was silently ignored by export and import, and an export run from a checkout carried the entire live corpus instead of the workspace the operator named. cmd_import carried the identical line. One seam, four callers. archive.py gains _resolve_project_dir, which routes through project_data_dir(None) so the env override, git-worktree inference, and gripspace inference apply identically to the archive verbs and to every other recall surface. It verifies the .synapt/recall suffix before stripping it, the same defense channel.py already uses, rather than a blind parents[1]. export_recall_archive and import_recall_archive accept None and resolve there. The CLI and MCP callers stop pretending to know the root. The CLI gains --path for the case where a caller genuinely does. Both verbs now report the RESOLVED store they touched, recorded after inference, never the caller's argument. Export writes data_dir into the manifest -- the directory the bytes came from -- and prints it. Import returns data_dir in its summary -- the destination the bytes landed in -- and prints it, while the archive's own origin store travels as source_data_dir so the two are never conflated: echoing the manifest's field would name where the bytes came from and read like a destination. A bare chunk count with no path is exactly the report shape that let a wrong-store export hide; an argument-derived path would let an inference-redirected --path hide the same way. Six witnesses. Export and import against the env override, two roots each, both mutation-killed by reverting the resolver to cwd. Export reporting against a real linked worktree with a liveness control, mutation-killed by recording the argument instead of the resolved dir. Import reporting from a source root, under an override root, standing in a third, driving both the CLI print and the MCP text, mutation-killed by echoing the archive's source store. Absent provenance in both import modes, against a real pre-provenance archive rewritten from a current one, with the current archive as the control: the key is omitted rather than None, mutation-killed by dropping the guard. Same-shell fruit: a configuration that previously exported the entire live corpus now exports the one planted entry and names the store it read. Co-Authored-By: Claude <noreply@anthropic.com>
…all-root fix(recall): export/import resolve the recall root, never the cwd
…ucket fix(journal): stabilize worktree bucket resolution
…ain` Three specs in one seam, all of which needed the parser to be testable first. PARSER EXTRACTED. `make_parser()` now builds what `main()` used to build inline. That is not incidental refactoring: while the parser was constructed inside main(), no test could ask what a bare `synapt build` does, which is how the CLI came to default to a FULL rebuild while MCP `recall_build` defaulted to incremental -- the same operation with opposite defaults depending on which surface you reached through, with nothing red anywhere. test_cli_and_mcp_defaults_agree now pins the two together. DEFAULT FLIPPED. Incremental is the default; `--full` is the explicit opt-out and sets BOTH fields rather than leaving one derived from the other, because two fields that must agree are two chances to disagree and this pair has already diverged once. `--incremental` still parses, for scripts that pass it. A NO-OP RUN SAYS SO -- AND SAYS ONLY WHAT IS TRUE, WITNESSED. A fast build and a broken build are indistinguishable from outside unless the build states which stages it skipped, so the short-circuit reports as well as returning early, and returns a usable index because callers dereference it. The line names parse, enrich and index, NOT archive: archiving is Step 1 and has already run by the time the check fires. It copied nothing, which is precisely why the signature matches, but a stage that executed must not be reported as skipped. An operator reads that line and stops looking. The corrected line is WITNESSED. Section F's existing test asserts only that "up to date" and "skip" appear, so it passes on the truthful line and on the false one alike -- verified: restoring the false wording reds ONLY the new test_the_skip_line_does_not_name_a_stage_that_actually_ran while every other skip-related test stays green. A correction nobody can regress is a correction that comes back. THE FINDING, measured rather than reasoned: THE BUILD MUTATES ONE OF ITS OWN SIGNED INPUTS. It synthesizes auto-journal stubs, so the journal on disk after a build differs from the journal it read. Storing the pre-build signature therefore makes the next run compute a different digest from a workspace nobody touched, and the no-op can NEVER fire -- pre 26636950, post f4d4491d, measured on an untouched store. So the signature is recomputed after the build rather than reused from before it. `_build_journal_files` extracted so the signature and the build read the same set. Two independent derivations of "the inputs" are two chances to disagree, and a signature covering fewer files than the build reads yields a FALSE no-op: the build reports "up to date" while real content sits unindexed, the one failure direction with no external symptom. AND THE RECOMPUTE OPENED A SECOND FALSE NO-OP, which took three review rounds to close properly. Recomputing at the end also picks up ARRIVALS: anything landing between the read and the manifest write is stat'd into the stored signature while never having been read, so the next run compares equal, prints "Up to date", and that content stays invisible until some unrelated change moves the digest. Three distinct paths, all measured, all found by review: a transcript dropped after the index save; a journal entry appended after the index save; and an entry landing after the guards had sampled but before a LATER call computed the value actually written to the manifest. The first two fixes were patches on the symptom. Excluding journals from the guard suppressed a false positive and surrendered every true positive with it, and adding a per-class journal check left the compared samples and the persisted sample as different objects -- so the guard verified one value and stored another, which verifies nothing about what was stored. WHAT IT NEEDED WAS ONE SAMPLE, TAKEN EARLY, VALIDATED LATE. A single full signature is computed at the READ BOUNDARY -- stub synthesis complete, journal parsing not yet begun, the exact line where the build's own writes end and somebody else's begin -- and THAT OBJECT is what gets persisted. Two comparisons decide only whether to keep it: read-only inputs must not have moved between the start of the run and the read boundary (journals excluded from this one alone, because the build writes them in that window), and the full input set must be unchanged between the read boundary and the end. Because the stored value is older than both comparisons, nothing arriving afterwards -- including after the check itself -- can be inside it. The distinction that matters is a property of TIME, before-the-read versus after-the-read, not of input class. SIGN WHAT THE BUILD PARSES, NOT WHAT IT COPIES FROM. Step 1 copies transcripts into the project archive and every later stage parses THAT archive. The signature was keyed on source_dirs -- the outside directories copied FROM -- and that is a different set. The gap is not theoretical: Codex rollouts are copied straight from the Codex sessions directory and never pass through source_dirs at all, so a Codex workspace printed "Archived 1 Codex transcript(s)" and "Up to date" in the same run and never parsed it. The same mis-keying mis-times ordinary transcripts: one written after Step 1's copy is present in source_dirs and absent from the archive, so it is signed as parsed without having been, and the next run copies it and then skips. All four signature computations are now keyed on build_sources. Both paths witnessed, both red on the prior revision. (Stromus, r1 on v6, offered as one shape closing both; taken as offered.) AN INPUT THE BUILD READS MUST BE AN INPUT THE SIGNATURE COVERS. The ChatGPT export is parsed into the index on every run and was absent from the signature, so replacing the archive wholesale left the digest unchanged and the next run skipped it. That is the narrower sibling of the coverage gap this same seam fixes for journals -- the same class, in the same patch, one input over. `maintain` is the grinder's explicit home, bounded by default, and REPORTS the backlog rather than draining it silently -- a queue being worked and a queue stuck look identical unless the number left is printed. It calls the grinder as a module attribute, not a from-imported name, so a runtime swap is not silently ignored. Evidence: 9 red specs green. The suite is GREEN -- 2929 passed, 25 skipped, 5 xfailed, 0 failed -- and the invocation is stated because the count is not reproducible without it: from synapt/, `.venv/bin/python -m pytest`, with the import resolving to this worktree's src/synapt/recall/__init__.py, printed before the run. An earlier revision of this message claimed 2978 passing with 5 environmental failures. That measurement was taken under a pyenv interpreter whose `synapt.recall` resolved to a DIFFERENT worktree entirely, so it described code this branch does not contain; the failures were that interpreter's, not this suite's. The correction is recorded rather than quietly replaced, because a count that moves without an explanation is indistinguishable from a count that was never checked. Mutation table, re-derived against the final shape rather than carried forward. Persisting the pre-build signature reds the two skip-line witnesses. Forcing the no-op unconditionally reds 28. Dropping archive coverage reds the archive witness alone. Dropping the read-boundary-to-end comparison, and persisting a fresh sample instead of the compared one, each red the persisted-signature witness -- they share it by construction, being two ways to break one property. Re-keying all four signature sites back to source_dirs reds the Codex and post-copy witnesses; re-keying only one of them reds the skip-line witnesses instead, because the pre-build and persisted samples must agree. EVERY GUARD IS WITNESSED, AND THE ONE THAT NEARLY WAS NOT IS WORTH NAMING. The start-to-read-boundary comparison originally guarded mid-parse transcript arrivals; keying on build_sources subsumed that case and the mutation stopped reddening anything, leaving it responsible for channels and the archive, which are parsed before the journal read boundary. My first witness for it resolved the channels directory by INFERENCE and was defeated by its own inference, which I misreported as test isolation preventing the witness. It does not: pinning store resolution through the explicit env seam lands it, and removing the guard reds that witness and nothing else. Both arrival witnesses survive every single mutation and go red only when the end comparison and the persist-the-compared-object property are removed TOGETHER: two independent mechanisms each covering that case, measured rather than asserted. All restores verified against a clean baseline. Co-Authored-By: Claude <noreply@anthropic.com>
…t-and-maintain feat(build): incremental by default, a no-op that says so, and `maintain`
…imestamps fix(journal): normalize timestamp ordering
The Claude session-start prompt opened with "SessionStart:resume hook success" while performing no resume, and instructed a cron-backed monitoring loop that is no longer the intended pattern. The two halves were invisible for different reasons, and only one was defended. test_startup.py asserted `"CronCreate for the loop" in prompt` under the docstring "Claude agents keep the existing cron-backed monitoring instruction" -- so the suite reported agreement with an instruction that had already been withdrawn. A green test pinning a retired instruction is worse than no test: it looks like corroboration. The false label was not defended, it was UNCHECKED: no assertion referenced it in either direction, so it simply survived. A thing a passing test does not contradict is not a thing that test defends. The label now states what the hook DID (startup context loaded) rather than naming a mechanism that does not run, and the prompt tells the agent to wait to be prompted and notify its coordinator on completion instead of polling on a cadence. Each half is mutation-proven separately, because stacked asserts short-circuit and one mutation would only have proven the first layer. Premium boundary: recall is OSS -- this is session-start context surfacing, no identity or org semantics.
…alse-resume fix(hook): stop announcing a resume the hook never performs
Version bump, a changelog entry, and one bug fix that belongs in the release rather than after it. WHY 0.19.0 AND NOT 0.18.1. 0.18.0 was published to PyPI at 2026-08-07T18:01:21Z. All 17 first-parent units in the range ca62880..bc594f3 landed after that instant, checked against each commit's date rather than inferred, so the published artifact contains none of them. PyPI forbids re-uploading a version. An earlier version of this message said 11 of those units landed after the publish date. That was wrong. 11 is a real count, of units carrying product changes, attached to a claim about timing where the answer is all 17. The range starts at the 0.18.0 promotion merge, four minutes before PyPI published, so no subset of it can be the post-publish set. Caught in review before anything shipped. COUNTING RULE. Over ca62880..bc594f3: 35 commits, 17 merges, 18 direct, 17 first-parent. Of the 17, twelve touch src/ and five touch only tests, docs, or fixtures; one of those five is a topology-repair merge carrying no content. THE FIX. tests/recall_store_isolation.py imported the POSIX-only pwd module at module scope. conftest.py imports that module at root scope, so on Windows the ModuleNotFoundError fired before any collection at all, taking down the whole run rather than one module. The import now happens at call time; POSIX behaviour is byte-identical. Deliberately not replaced with Path.home(). That reads $HOME, which is exactly the value a fixture can move, and the protected boundary must not be derivable from the value under test or the guarantee becomes circular. On Windows it still raises, at the point of use, which is honest for a POSIX-only guarantee. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore: release 0.19.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ceremony promotion of
devtomainfor the 0.19.0 release.Promoted via a throwaway
promote/branch rather than makingdevthe PR head, becausedelete_branch_on_mergeis enabled and merging a dev-headed PR would delete the integration branch. Ancestry is preserved exactly.Content is already gated. The release commit passed a six-version public-push gate with two reviewers bound to head
d178530; the gate record is on #994. This PR adds no new bytes.Premium boundary: recall is OSS — memory storage, retrieval, and channel substrate. No identity or org semantics.